You are given N available cars and M rental requests. Each request is a tuple (pickupTime, returnTime, id):
pickupTime | when the customer picks up the car |
returnTime | when the customer returns the car |
id | a unique request identifier |
A car may serve multiple requests as long as their time intervals do not overlap. If one request returns a car at time t and another picks up a car at exactly time t, they may use the same car.
Assign a car to every request while satisfying all of the following:
| 1 | Output the assigned car ID for every request. |
| 2 | Minimise the number of cars used. |
| 3 | Design a Car class that stores a car ID and its assigned orders. |
| 4 | Car IDs start at 1 and are assigned in the order cars are first created. |
K available cars. If all requests can be served with at most K cars, output the assignment. Otherwise output IMPOSSIBLE. Reported constraints: 1 <= n <= 200,000, 1 <= K <= n, 0 <= start < end <= 10^9, all IDs distinct.# input: n=4 orders, K=2 cars # id start end 101 1 4 102 2 5 103 4 6 104 5 7 # output 1: 101 103 2: 102 104
<= versus < decision in your heap check, and it's the most likely place to lose the round quietly.# input: n=3 orders, K=1 car # id start end 1 1 3 2 2 4 3 4 5 # output IMPOSSIBLE
[2, 3), so two cars are needed at minimum. With K=1 there is no valid assignment. Note that orders 1 and 3 would share a car happily โ the infeasibility comes from a single overlapping pair, not from the whole set.The algorithm is Meeting Rooms II (#253) with an output requirement bolted on. If you've done #253, the scheduling half is solved.
The class requirement is the real content. "Design a Car class that stores a car ID and its assigned orders" is an unusual instruction for a Google coding round, and it forces three things at once โ a per-instance mutable list, an ordering so the objects go straight into heapq, and a separate stable-order list for output because the heap reorders itself. That's the whole reason this cheatsheet exists.
class Car: def __init__(self, car_id: int): self.id = car_id # fields are just assignments in __init__ self.orders: List[int] = [] # fresh list per instance self.free_at = 0 def assign(self, order_id: int, end: int) -> None: self.orders.append(order_id) self.free_at = end car = Car(1) car.assign(101, 4)
That is a complete, interview-acceptable class. No constructor keyword, no new, no field declarations above __init__. Fields spring into existence when you assign to self.something.
self is not magic and not optional. It's the instance, passed as the first argument automatically. You write it in every method signature; you never pass it at the call site. Forgetting it is the single most common syntax error under pressure.| Do I need a constructor? | Only if the object has state to initialise. __init__ is the constructor. If there's nothing to set, skip it entirely. |
| Do I declare fields first? | No. Assign them in __init__. Type hints on the assignment are optional and read as senior. |
| Public / private? | No enforcement in Python. A leading underscore _free_at is a convention meaning "internal." Don't bother in a 45-minute round. |
| Getters and setters? | No. Access the attribute directly. Writing Java-style getters in Python is a mild negative signal. |
Return type of __init__? | None. It mutates self; it does not return the object. |
| Inheritance? | Almost never needed in a coding round. If asked: class SUV(Car): then super().__init__(car_id) inside its __init__. |
# WRONG โ orders is defined on the CLASS, so all cars share one list class Car: orders = [] # class attribute def __init__(self, car_id): self.id = car_id a, b = Car(1), Car(2) a.orders.append('x') print(b.orders) # ['x'] <-- b was never touched
# RIGHT โ orders is created per instance inside __init__ class Car: def __init__(self, car_id): self.id = car_id self.orders = [] # instance attribute
I ran the wrong version above and it really does print ['x']. This is the bug that silently produces a correct-looking answer with wrong output, and it's very hard to spot while an interviewer is watching.
def f(items=[]) reuses one list across every call. Use def f(items=None) then items = items or [].orders in __init__ rather than at class level so each car gets its own list."heapq__lt__ โ the only comparison a heap needsThe car rental question needs a min-heap of cars ordered by next-free time. heapq compares elements with <, so give the class a __lt__ and you can push the objects directly.
def __lt__(self, other: "Car") -> bool: return self.free_at < other.free_at
Two alternatives, both fine, and worth naming so the interviewer sees you know the options:
# 1. push a tuple, no __lt__ needed heapq.heappush(heap, (car.free_at, car.id, car)) # include a tiebreaker (car.id) so Python never has to # compare two Car objects when free_at ties -> TypeError # 2. dataclass with order=True (see below)
car.id in the middle, two equal free_at values make Python fall through to comparing the Car objects, and you get TypeError: '<' not supported. This is a classic live-interview crash.__repr__ โ free debuggingdef __repr__(self) -> str: return f"Car(id={self.id}, free_at={self.free_at}, orders={self.orders})"
Without it, print(car) gives <__main__.Car object at 0x104f2a3d0>, which is useless when you're dry-running by hand in a Google Doc. With it, printing a list of cars shows you the whole state at a glance.
__repr__ early and say "this makes the dry run readable." Interviewers notice.import heapq from typing import List, Optional, Tuple class Car: def __init__(self, car_id: int): self.id = car_id self.orders: List[int] = [] self.free_at = 0 def assign(self, order_id: int, end: int) -> None: self.orders.append(order_id) self.free_at = end def __lt__(self, other: "Car") -> bool: return self.free_at < other.free_at def __repr__(self) -> str: return f"Car(id={self.id}, free_at={self.free_at}, orders={self.orders})" def assign_cars(orders: List[Tuple[int, int, int]], k: Optional[int] = None) -> Optional[List[Car]]: """orders = [(id, start, end)]. Returns cars, or None if k is too small.""" orders = sorted(orders, key=lambda o: o[1]) # by pickup time heap: List[Car] = [] # cars in use, ordered by free_at cars: List[Car] = [] # every car, in creation order for oid, start, end in orders: if heap and heap[0].free_at <= start: # <= : return at t, pickup at t is OK car = heapq.heappop(heap) else: if k is not None and len(cars) >= k: return None # IMPOSSIBLE car = Car(len(cars) + 1) cars.append(car) car.assign(oid, end) heapq.heappush(heap, car) return cars
heap and cars are separate lists: the heap reorders itself constantly, so it can't give you cars in creation order for the output. cars holds stable insertion order. Both hold references to the same objects, so car.assign(...) is visible through either โ that's Python's reference semantics doing the work for you, and it's worth saying out loud.Complexity: O(n log n) for the sort plus O(n log n) heap operations. O(n) space.
assert, no frameworkYou have no test runner in a Google Doc or CoderPad. Plain asserts read as deliberate and cost three lines.
# example 1 from the problem: 4 orders, 2 cars res = assign_cars([(101,1,4), (102,2,5), (103,4,6), (104,5,7)], k=2) assert [c.orders for c in res] == [[101,103], [102,104]] # example 2: infeasible with 1 car assert assign_cars([(1,1,3), (2,2,4), (3,4,5)], k=1) is None # edge cases worth naming even if you don't code them assert assign_cars([]) == [] # empty input assert len(assign_cars([(1,1,2), (2,2,3)])) == 1 # touching intervals share a car
t and a pickup at time t may share a car. That's the difference between <= and < in your heap check, and it's exactly the kind of off-by-one an interviewer probes. Testing it proves you read the spec.| Empty input | No orders โ no cars, not a crash. |
| Single order | Exactly one car. |
| All overlapping | n orders all at once โ n cars. Worst case. |
| None overlapping | Sequential orders โ 1 car. Best case. |
| Touching at a boundary | End == next start. The <= vs < decision. |
| k too small / k = n | IMPOSSIBLE path, and the trivially-feasible path. |
| Unsorted input | Your sort handles it, but say that you rely on it. |
from dataclasses import dataclass, field @dataclass(order=True) class Car: free_at: int = 0 id: int = 0 orders: List[int] = field( default_factory=list, compare=False)
You get __init__, __repr__, __eq__ and (with order=True) all the comparisons for free, generated in field declaration order.
1. Field order is comparison order. free_at is listed first so heap ordering works. Reorder the fields and you silently change the sort key.
2. Mutable defaults need default_factory. Writing orders: List[int] = [] raises ValueError at class definition โ the dataclass module catches the shared-mutable trap for you. compare=False keeps the list out of comparisons.
Probably not. A plain class is three more lines and shows the interviewer you know what __init__ and __lt__ actually do. A dataclass hides exactly the mechanics being assessed when someone says "design a Car class."
__eq__ + __hash__ โ only when you need itBy default, two objects with identical fields are different keys, because the default hash is identity-based. If you need to dedupe objects in a set or use them as dict keys, define both.
class Point: def __init__(self, x, y): self.x, self.y = x, y def __eq__(self, o): return isinstance(o, Point) and (self.x, self.y) == (o.x, o.y) def __hash__(self): return hash((self.x, self.y)) len({Point(1,2), Point(1,2), Point(3,4)}) # 2
__eq__ alone sets __hash__ to None and your object becomes unhashable. Always define both or neither. | You want | Write |
|---|---|
| Constructor | def __init__(self, a, b): |
| Instance field | self.name = value inside __init__ |
| Method | def method(self, arg): โ self always first |
| Printable | def __repr__(self): return f"..." |
| Heap-sortable | def __lt__(self, other): return ... |
| Set / dict key | __eq__ and __hash__, always both |
Length via len() | def __len__(self): return len(self.orders) |
| Shared across instances | Class attribute โ only for immutables like MAX = 100 |
| No instance needed | @staticmethod (no self) or @classmethod (takes cls) |
| Alternate constructor | @classmethoddef from_tuple(cls, t): return cls(*t) |
| Inheritance | class SUV(Car): then super().__init__(car_id) |
| Forward type reference | def __lt__(self, other: "Car") โ quotes, class isn't defined yet |
| 1 | Forgetting self in a method signature. |
| 2 | Mutable class attribute shared across instances. |
| 3 | Pushing tuples to a heap without a tiebreaker โ TypeError on ties. |
| 4 | __eq__ without __hash__ โ object silently unhashable. |
| 5 | Calling Car.assign(order) on the class instead of an instance. |
| 6 | Expecting __init__ to return the object. It returns None. |
| 7 | Writing Java-style get_id() / set_id(). Just use the attribute. |
google-prep.html โ the car rental question is real and was asked twice in August 2026.